// External crates
use arc_swap::ArcSwapOption;
use atomic_float::AtomicF32;
use bytes::{BufMut, Bytes, BytesMut};
use clap::{Parser, Subcommand, ValueEnum};
use convert_case::{Case, Casing};
use crossterm::{
    cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
    event::{
        self, EventStream, KeyCode, KeyEventKind, KeyModifiers, KeyboardEnhancementFlags,
        PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
    },
    style::{Print, Stylize as _},
    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use eyre::{bail, eyre, OptionExt as _};
use futures::{
    stream::{FuturesOrdered, FuturesUnordered},
    FutureExt as _, StreamExt as _,
};
use indicatif::ProgressBar;
use mpris_server::{
    zbus::{self, fdo, Result as ZBusResult},
    LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
    Time, TrackId, Volume,
};
use regex::Regex;
use reqwest::Client;
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source as _};
use scraper::{Html, Selector};
use serde::{
    de::{self, Visitor},
    Deserialize, Deserializer,
};
use thiserror::Error;
use tokio::{
    fs::{self, File},
    io::{self, AsyncWriteExt},
    select,
    sync::{
        mpsc,
        mpsc::{error::SendError, Receiver, Sender},
        RwLock,
    },
    task,
    time::sleep,
};
use unicode_segmentation::UnicodeSegmentation;
use url::form_urlencoded;

// Standard library
use std::{
    cmp::min,
    collections::VecDeque,
    env,
    error::Error as StdError,
    ffi::NulError,
    fmt,
    fmt::Write as _,
    io::{stdout, Cursor, IsTerminal, Stdout},
    ops::Deref as _,
    panic,
    path::{Path, PathBuf},
    process,
    process::exit,
    str::FromStr,
    sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        Arc,
    },
    time::Duration,
};

// Constants
const ERROR: fdo::Error = fdo::Error::Failed(String::new());

#[derive(Parser, Clone)]
#[command(about, version)]
#[allow(clippy::struct_excessive_bools)]
struct Args {
    #[clap(long, short)]
    alternate: bool,
    #[clap(long, short)]
    minimalist: bool,
    #[clap(long, short)]
    borderless: bool,
    #[clap(long, short)]
    paused: bool,
    #[clap(long, short, default_value_t = 12)]
    fps: u8,
    #[clap(long, default_value_t = 3)]
    timeout: u64,
    #[clap(long, short)]
    debug: bool,
    #[clap(long, short, default_value_t = 3)]
    width: usize,
    #[clap(long, short, alias = "list", alias = "tracks", short_alias = 'l')]
    track_list: Option<String>,
    #[clap(long, short = 's', alias = "buffer", default_value_t = 5)]
    buffer_size: usize,
    #[command(subcommand)]
    command: Option<Commands>,
}
#[derive(Subcommand, Clone)]
enum Commands {
    Scrape { source: Source },
}
pub fn data_dir() -> eyre::Result<PathBuf, PlayerError> {
    let dir = dirs::data_dir().ok_or(PlayerError::DataDir)?.join("lowfi");
    Ok(dir)
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
    color_eyre::install()?;
    let cli = Args::parse();
    if let Some(command) = cli.command {
        match command {
            Commands::Scrape { source } => match source {
                Source::Archive => archive_scrape().await?,
                Source::Lofigirl => lofigirl_scrape().await?,
                Source::Chillhop => chillhop_scrape().await?,
            },
        }
    } else {
        play(cli).await?;
    };
    Ok(())
}

pub struct Player {
    pub sink: Sink,
    pub buffer_size: usize,
    current: ArcSwapOption<Info>,
    progress: AtomicF32,
    tracks: RwLock<VecDeque<QueuedTrack>>,
    pub bookmarks: Bookmarks,
    timeout: Duration,
    list: List,
    volume: PersistentVolume,
    client: Client,
}
impl Player {
    fn set_current(&self, info: Info) {
        self.current.store(Some(Arc::new(info)));
    }
    pub fn current_exists(&self) -> bool {
        self.current.load().is_some()
    }
    pub fn set_volume(&self, volume: f32) {
        self.sink.set_volume(volume.clamp(0.0, 1.0));
    }
    async fn new(args: &Args) -> eyre::Result<(Self, OutputStream), PlayerError> {
        let bookmarks = Bookmarks::load().await?;
        let volume = PersistentVolume::load()
            .await
            .map_err(PlayerError::PersistentVolumeLoad)?;
        let list = List::load(args.track_list.as_ref())
            .await
            .map_err(PlayerError::TrackListLoad)?;
        #[cfg(target_os = "linux")]
        let mut stream = if !args.alternate && !args.debug {
            silent_get_output_stream()?
        } else {
            OutputStreamBuilder::open_default_stream()?
        };
        #[cfg(not(target_os = "linux"))]
        let mut stream = OutputStreamBuilder::open_default_stream()?;
        stream.log_on_drop(false);
        let sink = Sink::connect_new(stream.mixer());
        if args.paused {
            sink.pause();
        }
        let client = Client::builder()
            .user_agent(concat!(
                env!("CARGO_PKG_NAME"),
                "/",
                env!("CARGO_PKG_VERSION")
            ))
            .timeout(Duration::from_secs(args.timeout * 5))
            .build()?;
        let player = Self {
            tracks: RwLock::new(VecDeque::with_capacity(args.buffer_size)),
            buffer_size: args.buffer_size,
            current: ArcSwapOption::new(None),
            progress: AtomicF32::new(0.0),
            timeout: Duration::from_secs(args.timeout),
            bookmarks,
            client,
            sink,
            volume,
            list,
        };
        Ok((player, stream))
    }
    pub async fn play(
        player: Arc<Self>,
        tx: Sender<Message>,
        mut rx: Receiver<Message>,
        debug: bool,
    ) -> eyre::Result<(), PlayerError> {
        let mpris = Server::new(Arc::clone(&player), tx.clone())
            .await
            .inspect_err(|x| {
                dbg!(x);
            })?;
        let downloader = Downloader::new(Arc::clone(&player));
        let (itx, downloader) = downloader.start(debug);
        Downloader::notify(&itx).await?;
        player.set_volume(player.volume.float());
        let mut new = false;
        loop {
            let clone = Arc::clone(&player);
            let msg = select! {
                biased;
                Some(x) = rx.recv() => x,
                Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()),
                        if new => Message::Next,
            };
            match msg {
                Message::Next | Message::Init | Message::TryAgain => {
                    new = false;
                    if msg == Message::Next && !player.current_exists() {
                        continue;
                    }
                    task::spawn(Self::next(
                        Arc::clone(&player),
                        itx.clone(),
                        tx.clone(),
                        debug,
                    ));
                }
                Message::Play => {
                    player.sink.play();

                    mpris.playback(PlaybackStatus::Playing).await?;
                }
                Message::Pause => {
                    player.sink.pause();

                    mpris.playback(PlaybackStatus::Paused).await?;
                }
                Message::PlayPause => {
                    if player.sink.is_paused() {
                        player.sink.play();
                    } else {
                        player.sink.pause();
                    }

                    mpris
                        .playback(mpris.player().playback_status().await?)
                        .await?;
                }
                Message::ChangeVolume(change) => {
                    player.set_volume(player.sink.volume() + change);

                    mpris
                        .changed(vec![Property::Volume(player.sink.volume().into())])
                        .await?;
                }
                Message::NewSong => {
                    new = true;

                    mpris
                        .changed(vec![
                            Property::Metadata(mpris.player().metadata().await?),
                            Property::PlaybackStatus(mpris.player().playback_status().await?),
                        ])
                        .await?;
                    continue;
                }
                Message::Bookmark => {
                    let current = player.current.load();
                    let current = current.as_ref().unwrap();
                    player.bookmarks.bookmark(current).await?;
                }
                Message::Quit => break,
            }
        }
        downloader.abort();
        Ok(())
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)]
pub enum Source {
    Lofigirl,
    Archive,
    Chillhop,
}
impl Source {
    pub fn cache_dir(&self) -> &'static str {
        match self {
            Source::Lofigirl => "lofigirl",
            Source::Archive => "archive",
            Source::Chillhop => "chillhop",
        }
    }
    pub fn url(&self) -> &'static str {
        match self {
            Source::Chillhop => "https://chillhop.com",
            Source::Archive => "https://ia601004.us.archive.org/31/items/lofigirl",
            Source::Lofigirl => "https://lofigirl.com/wp-content/uploads",
        }
    }
}
async fn get(client: &Client, path: &str, source: Source) -> eyre::Result<String> {
    let trimmed = path.trim_matches('/');
    let cache = PathBuf::from(format!("./cache/{}/{trimmed}.html", source.cache_dir()));
    if let Ok(x) = fs::read_to_string(&cache).await {
        Ok(x)
    } else {
        let resp = client
            .get(format!("{}/{trimmed}", source.url()))
            .send()
            .await?;
        let status = resp.status();
        if status == 429 {
            bail!("rate limit reached: {path}");
        }
        if status != 404 && !status.is_success() && !status.is_redirection() {
            bail!("non success code {}: {path}", resp.status().as_u16());
        }
        let text = resp.text().await?;
        let parent = cache.parent();
        if let Some(x) = parent {
            if x != Path::new("") {
                fs::create_dir_all(x).await?;
            }
        }
        let mut file = File::create(&cache).await?;
        file.write_all(text.as_bytes()).await?;
        if status.is_redirection() {
            bail!("redirect: {path}")
        }
        if status == 404 {
            bail!("not found: {path}")
        }
        Ok(text)
    }
}

pub type DecodedData = Decoder<Cursor<Bytes>>;
#[derive(Debug, Clone)]
pub enum TrackName {
    Raw(String),
    Formatted(String),
}
pub struct QueuedTrack {
    pub name: TrackName,
    pub full_path: String,
    pub data: Bytes,
}
impl QueuedTrack {
    pub fn decode(self) -> eyre::Result<DecodedTrack, Error> {
        DecodedTrack::new(self)
    }
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Info {
    pub full_path: String,
    pub custom_name: bool,
    pub display_name: String,
    pub width: usize,
    pub duration: Option<Duration>,
}

impl Info {
    pub fn to_entry(&self) -> String {
        let mut entry = self.full_path.clone();
        if self.custom_name {
            entry.push('!');
            entry.push_str(&self.display_name);
        }
        entry
    }
    fn decode_url(text: &str) -> String {
        #[allow(clippy::tuple_array_conversions)]
        form_urlencoded::parse(text.as_bytes())
            .map(|(key, val)| [key, val].concat())
            .collect()
    }
    fn format_name(name: &str) -> eyre::Result<String, Error> {
        let path = Path::new(name);
        let name = path
            .file_stem()
            .and_then(|x| x.to_str())
            .ok_or((name, Kind::InvalidName))?;
        let name = Self::decode_url(name).to_lowercase();
        let mut name = name
            .replace("masster", "master")
            .replace("(online-audio-converter.com)", "")
            .replace('_', " ");
        for regex in [
            Regex::new(r"\s*\(.*?master(?:\s*v?\d+)?\)$").unwrap(),
            Regex::new(r"\s*[-(]?\s*mstr(?:\s*v?\d+)?\s*\)?$").unwrap(),
            Regex::new(r"\s*[-]?\s*master(?:\s*v?\d+)?$").unwrap(),
            Regex::new(r"\s+kupla\s+master(?:\s*v?\d+|\d+)?$").unwrap(),
            Regex::new(r"\s*\(.*?master(?:\s*v?\d+)?\)(?:\s*\(\d+\))+$").unwrap(),
        ]
        .iter()
        {
            name = regex.replace(&name, "").to_string();
        }
        name = Regex::new(r"^[a-z]\d[ .]")
            .unwrap()
            .replace(&name, "")
            .to_string();
        let name = name
            .replace("13lufs", "")
            .to_case(Case::Title)
            .replace(" .", "")
            .replace(" Ft ", " ft. ")
            .replace("Ft.", "ft.")
            .replace("Feat.", "ft.")
            .replace(" W ", " w/ ");
        let mut skip = 0;
        for character in name.as_bytes() {
            if character.is_ascii_digit()
                || *character == b'.'
                || *character == b')'
                || *character == b'('
            {
                skip += 1;
            } else {
                break;
            }
        }
        if skip == name.len() {
            Ok(name.trim().to_string())
        } else {
            #[allow(clippy::string_slice)]
            Ok(String::from(name[skip..].trim()))
        }
    }
    pub fn new(
        name: TrackName,
        full_path: String,
        decoded: &DecodedData,
    ) -> eyre::Result<Self, Error> {
        let (display_name, custom_name) = match name {
            TrackName::Raw(raw) => (Self::format_name(&raw)?, false),
            TrackName::Formatted(custom) => (custom, true),
        };
        Ok(Self {
            duration: decoded.total_duration(),
            width: display_name.graphemes(true).count(),
            full_path,
            custom_name,
            display_name,
        })
    }
}
pub struct DecodedTrack {
    pub info: Info,
    pub data: DecodedData,
}
impl DecodedTrack {
    pub fn new(track: QueuedTrack) -> eyre::Result<Self, Error> {
        let data = Decoder::builder()
            .with_byte_len(track.data.len().try_into().unwrap())
            .with_data(Cursor::new(track.data))
            .build()
            .track(track.full_path.clone())?;
        let info = Info::new(track.name, track.full_path, &data)?;
        Ok(Self { info, data })
    }
}

async fn play(args: Args) -> eyre::Result<(), PlayerError> {
    let eyre_hook = panic::take_hook();
    panic::set_hook(Box::new(move |x| {
        let mut lock = stdout().lock();
        crossterm::execute!(
            lock,
            Clear(ClearType::FromCursorDown),
            Show,
            PopKeyboardEnhancementFlags
        )
        .unwrap();
        terminal::disable_raw_mode().unwrap();
        eyre_hook(x);
        exit(1)
    }));
    let (player, stream) = Player::new(&args).await?;
    let player = Arc::new(player);
    let (tx, rx) = mpsc::channel(8);
    let ui = if stdout().is_terminal() && !(env::var("LOWFI_DISABLE_UI") == Ok("1".to_owned())) {
        Some(task::spawn(start(
            Arc::clone(&player),
            tx.clone(),
            args.clone(),
        )))
    } else {
        None
    };
    tx.send(Message::Init).await?;
    Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?;
    PersistentVolume::save(player.sink.volume())
        .await
        .map_err(PlayerError::PersistentVolumeSave)?;
    player.bookmarks.save().await?;
    drop(stream);
    player.sink.stop();
    if let Some(x) = ui {
        x.abort();
    }
    Ok(())
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum Message {
    Next,
    NewSong,
    TryAgain,
    Init,
    #[allow(dead_code, reason = "this code may not be dead depending on features")]
    Play,
    Pause,
    PlayPause,
    ChangeVolume(f32),
    Bookmark,
    Quit,
}
pub fn silent_get_output_stream() -> eyre::Result<rodio::OutputStream, PlayerError> {
    use libc::freopen;
    use rodio::OutputStreamBuilder;
    use std::ffi::CString;
    extern "C" {
        static stderr: *mut libc::FILE;
    }
    let mode = CString::new("w")?;
    let null = CString::new("/dev/null")?;
    unsafe {
        freopen(null.as_ptr(), mode.as_ptr(), stderr);
    }
    let stream = OutputStreamBuilder::open_default_stream()?;
    let tty = CString::new("/dev/tty")?;
    unsafe {
        freopen(tty.as_ptr(), mode.as_ptr(), stderr);
    }
    Ok(stream)
}
use tokio::task::JoinHandle;
pub struct Downloader {
    player: Arc<Player>,
    rx: Receiver<()>,
    tx: Sender<()>,
}
impl Downloader {
    pub async fn notify(sender: &Sender<()>) -> Result<(), mpsc::error::SendError<()>> {
        sender.send(()).await
    }
    pub fn new(player: Arc<Player>) -> Self {
        let (tx, rx) = mpsc::channel(8);
        Self { player, rx, tx }
    }
    pub async fn push_buffer(&self, debug: bool) {
        let data = self.player.list.random(&self.player.client, None).await;
        match data {
            Ok(track) => self.player.tracks.write().await.push_back(track),
            Err(error) => {
                if debug {
                    panic!("{error} - {:?}", error.source())
                }
                if !error.is_timeout() {
                    sleep(self.player.timeout).await;
                }
            }
        }
    }
    pub fn start(mut self, debug: bool) -> (Sender<()>, JoinHandle<()>) {
        let tx = self.tx.clone();
        let handle = task::spawn(async move {
            while self.rx.recv().await == Some(()) {
                while self.player.tracks.read().await.len() < self.player.buffer_size {
                    self.push_buffer(debug).await;
                }
            }
        });
        (tx, handle)
    }
}

pub struct MprisPlayer {
    pub player: Arc<Player>,
    pub sender: Sender<Message>,
}
impl RootInterface for MprisPlayer {
    async fn raise(&self) -> fdo::Result<()> {
        Err(ERROR)
    }
    async fn quit(&self) -> fdo::Result<()> {
        self.sender
            .send(Message::Quit)
            .await
            .map_err(|_error| ERROR)
    }
    async fn can_quit(&self) -> fdo::Result<bool> {
        Ok(true)
    }
    async fn fullscreen(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn set_fullscreen(&self, _: bool) -> ZBusResult<()> {
        Ok(())
    }
    async fn can_set_fullscreen(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn can_raise(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn has_track_list(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn identity(&self) -> fdo::Result<String> {
        Ok("lowfi".to_owned())
    }
    async fn desktop_entry(&self) -> fdo::Result<String> {
        Ok("dev.talwat.lowfi".to_owned())
    }
    async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> {
        Ok(vec!["https".to_owned()])
    }
    async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> {
        Ok(vec!["audio/mpeg".to_owned()])
    }
}
impl PlayerInterface for MprisPlayer {
    async fn next(&self) -> fdo::Result<()> {
        self.sender
            .send(Message::Next)
            .await
            .map_err(|_error| ERROR)
    }
    async fn previous(&self) -> fdo::Result<()> {
        Err(ERROR)
    }
    async fn pause(&self) -> fdo::Result<()> {
        self.sender
            .send(Message::Pause)
            .await
            .map_err(|_error| ERROR)
    }
    async fn play_pause(&self) -> fdo::Result<()> {
        self.sender
            .send(Message::PlayPause)
            .await
            .map_err(|_error| ERROR)
    }
    async fn stop(&self) -> fdo::Result<()> {
        self.pause().await
    }
    async fn play(&self) -> fdo::Result<()> {
        self.sender
            .send(Message::Play)
            .await
            .map_err(|_error| ERROR)
    }
    async fn seek(&self, _offset: Time) -> fdo::Result<()> {
        Err(ERROR)
    }
    async fn set_position(&self, _track_id: TrackId, _position: Time) -> fdo::Result<()> {
        Err(ERROR)
    }
    async fn open_uri(&self, _uri: String) -> fdo::Result<()> {
        Err(ERROR)
    }
    async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
        Ok(if !self.player.current_exists() {
            PlaybackStatus::Stopped
        } else if self.player.sink.is_paused() {
            PlaybackStatus::Paused
        } else {
            PlaybackStatus::Playing
        })
    }
    async fn loop_status(&self) -> fdo::Result<LoopStatus> {
        Err(ERROR)
    }
    async fn set_loop_status(&self, _loop_status: LoopStatus) -> ZBusResult<()> {
        Ok(())
    }
    async fn rate(&self) -> fdo::Result<PlaybackRate> {
        Ok(self.player.sink.speed().into())
    }
    async fn set_rate(&self, rate: PlaybackRate) -> ZBusResult<()> {
        self.player.sink.set_speed(rate as f32);
        Ok(())
    }
    async fn shuffle(&self) -> fdo::Result<bool> {
        Ok(true)
    }
    async fn set_shuffle(&self, _shuffle: bool) -> ZBusResult<()> {
        Ok(())
    }
    async fn metadata(&self) -> fdo::Result<Metadata> {
        let metadata = self
            .player
            .current
            .load()
            .as_ref()
            .map_or_else(Metadata::new, |track| {
                let mut metadata = Metadata::builder()
                    .title(track.display_name.clone())
                    .album(self.player.list.name.clone())
                    .build();
                metadata.set_length(
                    track
                        .duration
                        .map(|x| Time::from_micros(x.as_micros() as i64)),
                );
                metadata
            });
        Ok(metadata)
    }
    async fn volume(&self) -> fdo::Result<Volume> {
        Ok(self.player.sink.volume().into())
    }
    async fn set_volume(&self, volume: Volume) -> ZBusResult<()> {
        self.player.set_volume(volume as f32);
        flash_audio();
        Ok(())
    }
    async fn position(&self) -> fdo::Result<Time> {
        Ok(Time::from_micros(
            self.player.sink.get_pos().as_micros() as i64
        ))
    }
    async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
        Ok(0.2f64)
    }
    async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
        Ok(3.0f64)
    }
    async fn can_go_next(&self) -> fdo::Result<bool> {
        Ok(true)
    }
    async fn can_go_previous(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn can_play(&self) -> fdo::Result<bool> {
        Ok(true)
    }
    async fn can_pause(&self) -> fdo::Result<bool> {
        Ok(true)
    }
    async fn can_seek(&self) -> fdo::Result<bool> {
        Ok(false)
    }
    async fn can_control(&self) -> fdo::Result<bool> {
        Ok(true)
    }
}
pub struct Server {
    inner: mpris_server::Server<MprisPlayer>,
}
impl Server {
    pub async fn changed(
        &self,
        properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
    ) -> ZBusResult<()> {
        self.inner.properties_changed(properties).await
    }
    pub async fn playback(&self, new: PlaybackStatus) -> zbus::Result<()> {
        self.inner
            .properties_changed(vec![Property::PlaybackStatus(new)])
            .await
    }
    pub fn player(&self) -> &MprisPlayer {
        self.inner.imp()
    }
    pub async fn new(
        player: Arc<Player>,
        sender: Sender<Message>,
    ) -> eyre::Result<Self, zbus::Error> {
        let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
            String::from("lowfi")
        } else {
            format!("lowfi.{}.instance{}", player.list.name, process::id())
        };
        let server = mpris_server::Server::new(&suffix, MprisPlayer { player, sender }).await?;
        Ok(Self { inner: server })
    }
}

impl Player {
    async fn fetch(&self) -> Result<DecodedTrack, Error> {
        let track = self.tracks.write().await.pop_front();
        let track = if let Some(track) = track {
            track
        } else {
            self.current.store(None);
            self.progress.store(0.0, Ordering::Relaxed);
            self.list.random(&self.client, Some(&self.progress)).await?
        };
        let decoded = track.decode()?;
        self.set_current(decoded.info.clone());
        Ok(decoded)
    }
    pub async fn next(
        player: Arc<Self>,
        itx: Sender<()>,
        tx: Sender<Message>,
        debug: bool,
    ) -> eyre::Result<()> {
        player.sink.stop();
        let track = player.fetch().await;
        match track {
            Ok(track) => {
                player.sink.append(track.data);
                player.bookmarks.set_bookmarked(&track.info).await;
                Downloader::notify(&itx).await?;
                tx.send(Message::NewSong).await?;
            }
            Err(error) => {
                if debug {
                    panic!("{error} - {:?}", error.source())
                }
                if !error.is_timeout() {
                    sleep(player.timeout).await;
                }
                tx.send(Message::TryAgain).await?;
            }
        };
        Ok(())
    }
}

#[derive(Debug, Error)]
pub enum UIError {
    #[error("unable to convert number")]
    Conversion(#[from] std::num::TryFromIntError),
    #[error("unable to write output")]
    Write(#[from] std::io::Error),
    #[error("sending message to backend from ui failed")]
    Communication(#[from] tokio::sync::mpsc::error::SendError<Message>),
}
const AUDIO_BAR_DURATION: usize = 10;

pub fn flash_audio() {
    AtomicUsize::new(0).store(1, Ordering::Relaxed);
}
pub struct Window {
    borderless: bool,
    borders: [String; 2],
    width: usize,
    out: Stdout,
}
impl Window {
    pub fn new(width: usize, borderless: bool) -> Self {
        let borders = if borderless {
            [String::new(), String::new()]
        } else {
            let middle = "─".repeat(width + 2);
            [format!("┌{middle}┐"), format!("└{middle}┘")]
        };
        Self {
            borders,
            borderless,
            width,
            out: stdout(),
        }
    }
    pub fn draw(&mut self, content: Vec<String>, space: bool) -> eyre::Result<(), UIError> {
        let len: u16 = content.len().try_into()?;
        let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
            let padding = if self.borderless { " " } else { "│" };
            let space = if space {
                " ".repeat(self.width.saturating_sub(x.graphemes(true).count()))
            } else {
                String::new()
            };
            write!(output, "{padding} {}{space} {padding}\r\n", x.reset()).unwrap();
            output
        });
        #[cfg(windows)]
        let (height, suffix) = (len + 2, "\r\n");
        #[cfg(not(windows))]
        let (height, suffix) = (len + 1, "");
        let rendered = format!("{}\r\n{menu}{}{suffix}", self.borders[0], self.borders[1]);
        crossterm::execute!(
            self.out,
            Clear(ClearType::FromCursorDown),
            MoveToColumn(0),
            Print(rendered),
            MoveToColumn(0),
            MoveUp(height),
        )?;
        Ok(())
    }
}
async fn interface(
    player: Arc<Player>,
    minimalist: bool,
    borderless: bool,
    debug: bool,
    fps: u8,
    width: usize,
) -> eyre::Result<(), UIError> {
    let mut window = Window::new(width, borderless || debug);
    loop {
        let current = player.current.load();
        let current = current.as_ref();
        let action = action(&player, current, width);
        let volume = player.sink.volume();
        let percentage = format!("{}%", (volume * 100.0).round().abs());
        let timer = AtomicUsize::new(0).load(Ordering::Relaxed);
        let middle = match timer {
            0 => progress_bar(&player, current, width - 16),
            _ => audio_bar(volume, &percentage, width - 17),
        };
        if timer > 0 && timer <= AUDIO_BAR_DURATION {
            AtomicUsize::new(0).fetch_add(1, Ordering::Relaxed);
        } else {
            AtomicUsize::new(0).store(0, Ordering::Relaxed);
        }
        let controls = controls(width);
        let menu = match (minimalist, debug, player.current.load().as_ref()) {
            (true, _, _) => vec![action, middle],
            (false, true, Some(x)) => vec![x.full_path.clone(), action, middle, controls],
            _ => vec![action, middle, controls],
        };
        window.draw(menu, false)?;
        let delta = 1.0 / f32::from(fps);
        sleep(Duration::from_secs_f32(delta)).await;
    }
}
pub struct Environment {
    enhancement: bool,
    alternate: bool,
}
impl Environment {
    pub fn ready(alternate: bool) -> eyre::Result<Self, UIError> {
        let mut lock = stdout().lock();
        crossterm::execute!(lock, Hide)?;
        if alternate {
            crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?;
        }
        terminal::enable_raw_mode()?;
        let enhancement = terminal::supports_keyboard_enhancement()?;
        if enhancement {
            crossterm::execute!(
                lock,
                PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
            )?;
        }
        Ok(Self {
            enhancement,
            alternate,
        })
    }
    pub fn cleanup(&self) -> eyre::Result<(), UIError> {
        let mut lock = stdout().lock();
        if self.alternate {
            crossterm::execute!(lock, LeaveAlternateScreen)?;
        }
        crossterm::execute!(lock, Clear(ClearType::FromCursorDown), Show)?;
        if self.enhancement {
            crossterm::execute!(lock, PopKeyboardEnhancementFlags)?;
        }
        terminal::disable_raw_mode()?;

        eprintln!(
            "\n=====================\nLowfi quit ok. Bye, see ya! :)\n=====================\n"
        );
        Ok(())
    }
}
impl Drop for Environment {
    fn drop(&mut self) {
        let _ = self.cleanup();
    }
}
async fn start(
    player: Arc<Player>,
    sender: Sender<Message>,
    args: Args,
) -> eyre::Result<(), UIError> {
    let environment = Environment::ready(args.alternate)?;
    let interface = task::spawn(interface(
        Arc::clone(&player),
        args.minimalist,
        args.borderless,
        args.debug,
        args.fps,
        21 + args.width.min(32) * 2,
    ));
    listen(sender.clone()).await?;
    interface.abort();
    environment.cleanup()?;
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum BookmarkError {
    #[error("data directory not found")]
    DataDir,
    #[error("io failure")]
    Io(#[from] io::Error),
}
pub struct Bookmarks {
    entries: RwLock<Vec<String>>,
    bookmarked: AtomicBool,
}
impl Bookmarks {
    pub async fn path() -> eyre::Result<PathBuf, BookmarkError> {
        let data_dir = data_dir().map_err(|_| BookmarkError::DataDir)?;
        fs::create_dir_all(data_dir.clone()).await?;
        Ok(data_dir.join("bookmarks.txt"))
    }
    pub async fn load() -> eyre::Result<Self, BookmarkError> {
        let text = fs::read_to_string(Self::path().await?)
            .await
            .unwrap_or_default();
        let lines: Vec<String> = text
            .trim_start_matches("noheader")
            .trim()
            .lines()
            .filter_map(|x| {
                if x.is_empty() {
                    None
                } else {
                    Some(x.to_string())
                }
            })
            .collect();
        Ok(Self {
            entries: RwLock::new(lines),
            bookmarked: AtomicBool::new(false),
        })
    }
    pub async fn save(&self) -> eyre::Result<(), BookmarkError> {
        let text = format!("noheader\n{}", self.entries.read().await.join("\n"));
        fs::write(Self::path().await?, text).await?;
        Ok(())
    }
    pub async fn bookmark(&self, track: &Info) -> eyre::Result<(), BookmarkError> {
        let entry = track.to_entry();
        let idx = self.entries.read().await.iter().position(|x| **x == entry);
        if let Some(idx) = idx {
            self.entries.write().await.remove(idx);
        } else {
            self.entries.write().await.push(entry);
        };
        self.bookmarked
            .swap(idx.is_none(), std::sync::atomic::Ordering::Relaxed);
        Ok(())
    }
    pub fn bookmarked(&self) -> bool {
        self.bookmarked.load(std::sync::atomic::Ordering::Relaxed)
    }
    pub async fn set_bookmarked(&self, track: &Info) {
        let val = self.entries.read().await.contains(&track.to_entry());
        self.bookmarked
            .swap(val, std::sync::atomic::Ordering::Relaxed);
    }
}

#[derive(thiserror::Error, Debug)]
pub enum PlayerError {
    #[error("unable to load the persistent volume")]
    PersistentVolumeLoad(eyre::Error),
    #[error("unable to save the persistent volume")]
    PersistentVolumeSave(eyre::Error),
    #[error("sending internal message failed")]
    Communication(#[from] SendError<Message>),
    #[error("unable to load track list")]
    TrackListLoad(eyre::Error),
    #[error("interfacing with audio failed")]
    Stream(#[from] rodio::StreamError),
    #[error("NUL error, if you see this, something has gone VERY wrong")]
    Nul(#[from] NulError),
    #[error("unable to send or prepare network request")]
    Reqwest(#[from] reqwest::Error),

    #[error("mpris bus error")]
    ZBus(#[from] zbus::Error),

    #[error("mpris fdo (zbus interface) error")]
    Fdo(#[from] fdo::Error),
    #[error("unable to notify downloader")]
    DownloaderNotify(#[from] SendError<()>),
    #[error("unable to find data directory")]
    DataDir,
    #[error("bookmarking load/unload failed")]
    Bookmark(#[from] BookmarkError),
}

#[derive(Clone, Copy)]
pub struct PersistentVolume {
    inner: u16,
}
impl PersistentVolume {
    async fn config() -> eyre::Result<PathBuf> {
        let config = dirs::config_dir()
            .ok_or_else(|| eyre!("Couldn't find config directory"))?
            .join(PathBuf::from("lowfi"));
        if !config.exists() {
            fs::create_dir_all(&config).await?;
        }
        Ok(config)
    }
    pub fn float(self) -> f32 {
        f32::from(self.inner) / 100.0
    }
    pub async fn load() -> eyre::Result<Self> {
        let config = Self::config().await?;
        let volume = config.join(PathBuf::from("volume.txt"));
        let volume = if volume.exists() {
            let contents = fs::read_to_string(volume).await?;
            let trimmed = contents.trim();
            let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed);
            stripped
                .parse()
                .map_err(|_error| eyre!("volume.txt file is invalid"))?
        } else {
            fs::write(&volume, "100").await?;
            100u16
        };
        Ok(Self { inner: volume })
    }
    pub async fn save(volume: f32) -> eyre::Result<()> {
        let config = Self::config().await?;
        let path = config.join(PathBuf::from("volume.txt"));
        #[expect(
            clippy::as_conversions,
            clippy::cast_sign_loss,
            clippy::cast_possible_truncation
        )]
        let percentage = (volume * 100.0).abs().round() as u16;
        fs::write(path, percentage.to_string()).await?;
        Ok(())
    }
}
pub fn format_duration(duration: &Duration) -> String {
    let seconds = duration.as_secs() % 60;
    let minutes = duration.as_secs() / 60;
    format!("{minutes:02}:{seconds:02}")
}
pub fn progress_bar(player: &Player, current: Option<&Arc<Info>>, width: usize) -> String {
    let mut duration = Duration::new(0, 0);
    let elapsed = if current.is_some() {
        player.sink.get_pos()
    } else {
        Duration::new(0, 0)
    };
    let mut filled = 0;
    if let Some(current) = current {
        if let Some(x) = current.duration {
            duration = x;
            let elapsed = elapsed.as_secs() as f32 / duration.as_secs() as f32;
            filled = (elapsed * width as f32).round() as usize;
        }
    };
    format!(
        " [{}{}] {}/{} ",
        "/".repeat(filled),
        " ".repeat(width.saturating_sub(filled)),
        format_duration(&elapsed),
        format_duration(&duration),
    )
}
pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String {
    let audio = (volume * width as f32).round() as usize;
    format!(
        " volume: [{}{}] {}{} ",
        "/".repeat(audio),
        " ".repeat(width.saturating_sub(audio)),
        " ".repeat(4usize.saturating_sub(percentage.len())),
        percentage,
    )
}
enum ActionBar {
    Paused(Info),
    Playing(Info),
    Loading(f32),
    Muted,
}
impl ActionBar {
    fn format(&self, star: bool) -> (String, usize) {
        let (word, subject) = match self {
            Self::Playing(x) => ("playing", Some((x.display_name.clone(), x.width))),
            Self::Paused(x) => ("paused", Some((x.display_name.clone(), x.width))),
            Self::Loading(progress) => {
                let progress = format!("{: <2.0}%", (progress * 100.0).min(99.0));
                ("loading", Some((progress, 3)))
            }
            Self::Muted => {
                let msg = "+ to increase volume";
                ("muted,", Some((String::from(msg), msg.len())))
            }
        };
        subject.map_or_else(
            || (word.to_owned(), word.len()),
            |(subject, len)| {
                (
                    format!("{} {}{}", word, if star { "*" } else { "" }, subject.bold()),
                    word.len() + 1 + len + usize::from(star),
                )
            },
        )
    }
}
pub fn action(player: &Player, current: Option<&Arc<Info>>, width: usize) -> String {
    let (main, len) = current
        .map_or_else(
            || ActionBar::Loading(player.progress.load(std::sync::atomic::Ordering::Acquire)),
            |info| {
                let info = info.deref().clone();
                if player.sink.volume() < 0.01 {
                    return ActionBar::Muted;
                }
                if player.sink.is_paused() {
                    ActionBar::Paused(info)
                } else {
                    ActionBar::Playing(info)
                }
            },
        )
        .format(player.bookmarks.bookmarked());
    if len > width {
        let chopped: String = main.graphemes(true).take(width + 1).collect();
        format!("{chopped}...")
    } else {
        format!("{}{}", main, " ".repeat(width - len))
    }
}
pub fn controls(width: usize) -> String {
    let controls = [["[s]", "kip"], ["[p]", "ause"], ["[q]", "uit"]];
    let len: usize = controls.concat().iter().map(|x| x.len()).sum();
    let controls = controls.map(|x| format!("{}{}", x[0].bold(), x[1]));
    let mut controls = controls.join(&" ".repeat((width - len) / (controls.len() - 1)));
    controls.push_str(match width % 2 {
        0 => " ",
        _ => "",
    });
    controls
}
pub async fn listen(sender: Sender<Message>) -> eyre::Result<(), UIError> {
    let mut reader = EventStream::new();
    loop {
        let Some(Ok(event::Event::Key(event))) = reader.next().fuse().await else {
            continue;
        };
        if event.kind == KeyEventKind::Release {
            continue;
        }
        let messages = match event.code {
            KeyCode::Up => Message::ChangeVolume(0.1),
            KeyCode::Right => Message::ChangeVolume(0.01),
            KeyCode::Down => Message::ChangeVolume(-0.1),
            KeyCode::Left => Message::ChangeVolume(-0.01),
            KeyCode::Char(character) => match character.to_ascii_lowercase() {
                'c' if event.modifiers == KeyModifiers::CONTROL => Message::Quit,
                'q' => Message::Quit,
                's' | 'n' | 'l' => Message::Next,
                'p' | ' ' => Message::PlayPause,
                '+' | '=' | 'k' => Message::ChangeVolume(0.1),
                '-' | '_' | 'j' => Message::ChangeVolume(-0.1),
                'b' => Message::Bookmark,
                _ => continue,
            },
            KeyCode::Media(media) => match media {
                event::MediaKeyCode::Pause
                | event::MediaKeyCode::Play
                | event::MediaKeyCode::PlayPause => Message::PlayPause,
                event::MediaKeyCode::Stop => Message::Pause,
                event::MediaKeyCode::TrackNext => Message::Next,
                event::MediaKeyCode::LowerVolume => Message::ChangeVolume(-0.1),
                event::MediaKeyCode::RaiseVolume => Message::ChangeVolume(0.1),
                event::MediaKeyCode::MuteVolume => Message::ChangeVolume(-1.0),
                _ => continue,
            },
            _ => continue,
        };
        if let Message::ChangeVolume(_) = messages {
            flash_audio();
        }
        sender.send(messages).await?;
    }
}

async fn parse(client: &Client, path: &str) -> eyre::Result<Vec<String>> {
    let document = get(client, path, Source::Lofigirl).await?;
    let html = Html::parse_document(&document);
    Ok(html
        .select(&Selector::parse("html > body > pre > a").unwrap())
        .skip(1)
        .map(|x| String::from(x.attr("href").unwrap()))
        .collect())
}
async fn scan() -> eyre::Result<Vec<String>> {
    let client = Client::new();
    let mut releases = parse(&client, "/").await?;
    releases.truncate(releases.len() - 4);
    let mut futures = FuturesOrdered::new();
    for release in releases {
        let client = client.clone();
        futures.push_back(async move {
            let items = parse(&client, &release).await.unwrap();
            items
                .into_iter()
                .filter_map(|x| {
                    if x.ends_with(".mp3") {
                        Some(format!("{release}{x}"))
                    } else {
                        None
                    }
                })
                .collect::<Vec<String>>()
        });
    }
    let mut files = Vec::new();
    while let Some(mut result) = futures.next().await {
        files.append(&mut result);
    }
    eyre::Result::Ok(files)
}
pub async fn archive_scrape() -> eyre::Result<()> {
    println!("{}/", Source::Lofigirl.url());
    let files = scan().await?;
    for file in files {
        println!("{file}");
    }
    Ok(())
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Track {
    title: String,
    #[serde(deserialize_with = "deserialize_u32_from_string")]
    file_id: u32,
    artists: String,
}
impl Track {
    pub fn clean(&mut self) {
        self.artists = html_escape::decode_html_entities(&self.artists).to_string();
        self.title = html_escape::decode_html_entities(&self.title).to_string();
    }
}
#[derive(Deserialize, Debug)]
struct Release {
    #[serde(skip)]
    pub path: String,
    #[serde(skip)]
    pub index: usize,
    pub tracks: Vec<Track>,
}
#[derive(thiserror::Error, Debug)]
enum ReleaseError {
    #[error("invalid release: {0}")]
    Invalid(#[from] eyre::Error),
}
impl Release {
    pub async fn scan(
        path: String,
        index: usize,
        client: Client,
        bar: ProgressBar,
    ) -> Result<Self, ReleaseError> {
        let content = get(&client, &path, Source::Chillhop).await?;
        let html = Html::parse_document(&content);
        let textarea = html
            .select(&Selector::parse("textarea").unwrap())
            .next()
            .ok_or(eyre!("unable to find textarea: {path}"))?;
        let mut release: Self = serde_json::from_str(&textarea.inner_html()).unwrap();
        release.path = path;
        release.index = index;
        release.tracks.reverse();
        bar.inc(release.tracks.len() as u64);
        Ok(release)
    }
}
async fn scan_page(
    number: usize,
    client: &Client,
    bar: ProgressBar,
) -> eyre::Result<Vec<impl futures::Future<Output = Result<Release, ReleaseError>>>> {
    let path = format!("releases/?page={number}");
    let content = get(client, &path, Source::Chillhop).await?;
    let html = Html::parse_document(&content);
    let release = Selector::parse(".table-body > a").unwrap();
    let elements = html.select(&release);
    Ok(elements
        .enumerate()
        .filter_map(|(i, x)| {
            let label = x
                .select(&Selector::parse("label").unwrap())
                .next()?
                .inner_html();
            if label == "Compilation" {
                return None;
            }
            Some(Release::scan(
                x.attr("href")?.to_string(),
                (number * 12) + i,
                client.clone(),
                bar.clone(),
            ))
        })
        .collect())
}
pub async fn chillhop_scrape() -> eyre::Result<()> {
    const PAGE_COUNT: usize = 40;
    const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
    const TRACK_COUNT: u64 = 1625;
    const IGNORED_TRACKS: [u32; 20] = [
        74707, 21655, 21773, 8172, 55397, 75135, 24827, 8141, 8157, 64052, 31612, 41956, 8001,
        9217, 55372, 8469, 7832, 10448, 9446, 9396,
    ];
    const IGNORED_ARTISTS: [&str; 1] = ["Kenji"];
    fs::create_dir_all("./cache/chillhop").await.unwrap();
    let client = Client::builder().user_agent(USER_AGENT).build().unwrap();
    let futures = FuturesUnordered::new();
    let bar = ProgressBar::new(TRACK_COUNT + (12 * (PAGE_COUNT as u64)));
    let mut errors = Vec::new();
    for page in 0..=PAGE_COUNT {
        bar.inc(12);
        for x in scan_page(page, &client, bar.clone()).await? {
            futures.push(x);
        }
    }
    let mut results: Vec<Result<Release, ReleaseError>> = futures.collect().await;
    bar.finish_and_clear();
    eprintln!("sorting...");
    results.sort_by_key(|x| if let Ok(x) = x { x.index } else { 0 });
    results.reverse();
    eprintln!("printing...");
    let mut printed = Vec::with_capacity(TRACK_COUNT as usize);
    for result in results {
        let release = match result {
            Ok(release) => release,
            Err(error) => {
                errors.push(error);
                continue;
            }
        };
        for mut track in release.tracks {
            if IGNORED_TRACKS.contains(&track.file_id) {
                continue;
            }
            if IGNORED_ARTISTS.contains(&track.artists.as_ref()) {
                continue;
            }
            if printed.contains(&track.file_id) {
                continue;
            }
            printed.push(track.file_id);
            track.clean();
            println!("{}!{}", track.file_id, track.title);
        }
    }
    eprintln!("-- ERROR REPORT --");
    for error in errors {
        eprintln!("{error}");
    }
    Ok(())
}
pub fn deserialize_u32_from_string<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
    D: Deserializer<'de>,
{
    struct U32FromStringVisitor;
    impl<'de> Visitor<'de> for U32FromStringVisitor {
        type Value = u32;
        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("a string containing an unsigned 32-bit integer")
        }
        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            u32::from_str(value).map_err(|_| {
                de::Error::invalid_value(
                    de::Unexpected::Str(value),
                    &"a valid unsigned 32-bit integer",
                )
            })
        }
    }
    deserializer.deserialize_str(U32FromStringVisitor)
}
#[allow(dead_code)]
async fn lofigirl_parse(client: &Client, path: &str) -> eyre::Result<Vec<String>> {
    let document = get(client, path, Source::Lofigirl).await?;
    let html = Html::parse_document(&document);
    Ok(html
        .select(&Selector::parse("html > body > pre > a").unwrap())
        .skip(5)
        .map(|x| String::from(x.attr("href").unwrap()))
        .collect())
}
#[allow(dead_code)]
async fn lofigirl_scan() -> eyre::Result<Vec<String>> {
    let client = Client::new();
    let items = parse(&client, "/").await?;
    let mut years: Vec<u32> = items
        .iter()
        .filter_map(|x| {
            let year = x.strip_suffix("/")?;
            year.parse().ok()
        })
        .collect();
    years.sort();
    let mut futures = FuturesOrdered::new();
    for year in years {
        let months = parse(&client, &year.to_string()).await?;
        for month in months {
            let client = client.clone();
            futures.push_back(async move {
                let path = format!("{}/{}", year, month);
                let items = parse(&client, &path).await.unwrap();
                items
                    .into_iter()
                    .filter_map(|x| {
                        if x.ends_with(".mp3") {
                            Some(format!("{path}{x}"))
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<String>>()
            });
        }
    }
    let mut files = Vec::new();
    while let Some(mut result) = futures.next().await {
        files.append(&mut result);
    }
    eyre::Result::Ok(files)
}
pub async fn lofigirl_scrape() -> eyre::Result<()> {
    let files = scan().await?;
    for file in files {
        println!("{file}");
    }
    Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum Kind {
    #[error("unable to decode: {0}")]
    Decode(#[from] rodio::decoder::DecoderError),
    #[error("invalid name")]
    InvalidName,
    #[error("invalid file path")]
    InvalidPath,
    #[error("unknown target track length")]
    UnknownLength,
    #[error("unable to read file: {0}")]
    File(#[from] std::io::Error),
    #[error("unable to fetch data: {0}")]
    Request(#[from] reqwest::Error),
}
#[derive(Debug, thiserror::Error)]
#[error("{kind} (track: {track})")]
pub struct Error {
    pub track: String,
    #[source]
    pub kind: Kind,
}
impl Error {
    pub fn is_timeout(&self) -> bool {
        if let Kind::Request(x) = &self.kind {
            x.is_timeout()
        } else {
            false
        }
    }
}
impl<T, E> From<(T, E)> for Error
where
    T: Into<String>,
    Kind: From<E>,
{
    fn from((track, err): (T, E)) -> Self {
        Self {
            track: track.into(),
            kind: Kind::from(err),
        }
    }
}
pub trait Context<T> {
    fn track(self, name: impl Into<String>) -> Result<T, Error>;
}
impl<T, E> Context<T> for Result<T, E>
where
    (String, E): Into<Error>,
    E: Into<Kind>,
{
    fn track(self, name: impl Into<String>) -> Result<T, Error> {
        self.map_err(|e| {
            let error = match e.into() {
                Kind::Request(e) => Kind::Request(e.without_url()),
                e => e,
            };
            (name.into(), error).into()
        })
    }
}

#[derive(Clone)]
pub struct List {
    #[allow(dead_code)]
    pub name: String,
    lines: Vec<String>,
    #[allow(dead_code)]
    pub path: Option<String>,
}
impl List {
    pub fn base(&self) -> &str {
        self.lines[0].trim()
    }
    fn random_path(&self) -> (String, Option<String>) {
        let random = fastrand::usize(1..self.lines.len());
        let line = self.lines[random].clone();
        if let Some((first, second)) = line.split_once('!') {
            (first.to_owned(), Some(second.to_owned()))
        } else {
            (line, None)
        }
    }
    async fn download(
        &self,
        track: &str,
        client: &Client,
        progress: Option<&AtomicF32>,
    ) -> Result<(Bytes, String), Error> {
        let full_path = if track.contains("://") {
            track.to_owned()
        } else {
            format!("{}{}", self.base(), track)
        };
        let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") {
            let path = if x.starts_with('~') {
                let home_path = dirs::home_dir().ok_or((track, Kind::InvalidPath))?;
                let home = home_path.to_str().ok_or((track, Kind::InvalidPath))?;
                x.replace('~', home)
            } else {
                x.to_owned()
            };
            let result = tokio::fs::read(path.clone()).await.track(track)?;
            result.into()
        } else {
            let response = client.get(full_path.clone()).send().await.track(track)?;
            if let Some(progress) = progress {
                let total = response
                    .content_length()
                    .ok_or((track, Kind::UnknownLength))?;
                let mut stream = response.bytes_stream();
                let mut bytes = BytesMut::new();
                let mut downloaded: u64 = 0;
                while let Some(item) = stream.next().await {
                    let chunk = item.track(track)?;
                    let new = min(downloaded + (chunk.len() as u64), total);
                    downloaded = new;
                    progress.store((new as f32) / (total as f32), Ordering::Relaxed);
                    bytes.put(chunk);
                }
                bytes.into()
            } else {
                response.bytes().await.track(track)?
            }
        };
        Ok((data, full_path))
    }
    pub async fn random(
        &self,
        client: &Client,
        progress: Option<&AtomicF32>,
    ) -> Result<QueuedTrack, Error> {
        let (path, custom_name) = self.random_path();
        let (data, full_path) = self.download(&path, client, progress).await?;
        let name = custom_name.map_or_else(|| TrackName::Raw(path.clone()), TrackName::Formatted);
        Ok(QueuedTrack {
            name,
            full_path,
            data,
        })
    }
    pub fn new(name: &str, text: &str, path: Option<&str>) -> Self {
        let lines: Vec<String> = text
            .trim_end()
            .lines()
            .map(|x| x.trim_end().to_owned())
            .collect();
        Self {
            lines,
            path: path.map(ToOwned::to_owned),
            name: name.to_owned(),
        }
    }
    pub async fn load(tracks: Option<&String>) -> eyre::Result<Self> {
        if let Some(arg) = tracks {
            let path = data_dir()?.join(format!("{arg}.txt"));
            let path = if path.exists() { path } else { arg.into() };
            let raw = fs::read_to_string(path.clone()).await?;
            let raw = raw
                .strip_prefix("noheader")
                .map_or(raw.as_ref(), |stripped| stripped);
            let name = path
                .file_stem()
                .and_then(|x| x.to_str())
                .ok_or_eyre("invalid track path")?;
            Ok(Self::new(name, raw, path.to_str()))
        } else {
            Ok(Self::new("chillhop", include_str!("../chillhop.txt"), None))
        }
    }
}
